<?php

use Helper\Logger;
use Http\HttpConnection;
use Net\Connection;
use Net\Event;
ini_set('display_errors', 1);
error_reporting(E_ALL ^ E_NOTICE);

require_once "Http/HttpConnection.php";

class HttpProxyServer {
    const LOG_FILE = 'http.access.log';
    
    public function __construct($port=55557, $address='0.0.0.0')
    {
        Logger::$LOG_FILE = self::LOG_FILE;
        $server = new HttpConnection();
        $server->listening($address, $port);
    
        if($server->getLastError()) {
            Logger::record("Linstening to $address:$port fail: ({$server->getLastError()}) {$server->getLastErrorMessage()}");
            return;
        }
        
        $server->onReady(function() use($address, $port) {
            Logger::record("Http Proxy service ready!");
            Logger::record("Listening in $address:$port");
        });
    
        $server->onNewConnect(function(Event $e) {
            /** @var HttpConnection $clientConnection */
            $clientConnection = $e->getConnection()->setFlag('client');
            Logger::record("New connect {$clientConnection}");
        
            $clientConnection->onRecv(function(Event $e) use ($clientConnection) {
                $content = $e->getMessage();
                
                if($clientConnection->getForwardTo()) {
                    $clientConnection->forward($content);
                    return;
                }
                
                $headInfo = HttpConnection::parseRequest($content);
                
                switch (strtoupper($headInfo['method'])) {
                    case 'CONNECT':
                        $host = $headInfo['head_params']['host'];
                        list($host, $port) = explode(':', $host, 2);
                        
                        if(empty($port) || empty($host)) {
                            $clientConnection->close();
                            return;
                        }
    
                        $ip = HttpConnection::queryIp($host);
                        
                        if(!$ip) {
                            $clientConnection->close();
                            return;
                        }
    
                        Logger::record("$clientConnection $host resolve to $ip");
    
                        $proxyConnection = new HttpConnection();
                        $proxyConnection->setFlag('proxy');
    
                        $proxyConnection->beforeConnect(function(Event $e) use($clientConnection, $proxyConnection) {
                            Logger::record("$proxyConnection connecting to {$e->getAddress()}:{$e->getPort()}");
                        });
    
                        $proxyConnection->onConnectFail(function() use($clientConnection, $host, $port) {
                            $clientConnection->send($this->buildProxyFailResponse("Proxy server can't connect to {$host}:$port"));
                        });
    
                        $proxyConnection->onConnect(function() use($clientConnection, $proxyConnection) {
                            $proxyConnection->setupTwoWayForwarding($clientConnection);
                            $clientConnection->send(HttpConnection::buildResponse([], HttpConnection::STATUS_SUCCESS, 'Connection Established'));
                        });
    
                        $proxyConnection->onClose(function() use($clientConnection, $proxyConnection) {
                            $clientConnection->close();
                        });
    
                        $proxyConnection->beforeClose(function() use ($proxyConnection) {
                            Logger::record("connect closed {$proxyConnection} ForwardSize={$proxyConnection->getForwardSizeHumanReadable()}");
                        });
    
                        $proxyConnection->onRecv(function(Event $e) use($proxyConnection) {
                            $proxyConnection->forward($e->getMessage());
                        });
    
                        $proxyConnection->connect($ip, $port);
                        break;
                    default:
                        $urlInfo = HttpConnection::parseUrl($headInfo['request_path']);
                        $host = $urlInfo['host'];
                        $port = $urlInfo['port'];
                        
                        $ip = HttpConnection::queryIp($host);
                        
                        if(!$ip) {
                            $clientConnection->send($this->buildProxyFailResponse("Proxy server can't resolve host {$host}"))->close();
                            return;
                        }
    
                        Logger::record("$clientConnection $host resolve to $ip");
                        
                        unset($urlInfo['host']);
                        unset($urlInfo['port']);
                        unset($urlInfo['user']);
                        unset($urlInfo['pass']);
                        unset($urlInfo['scheme']);
                        
                        $headInfo['request_path'] = HttpConnection::unparseUrl($urlInfo);
                        
                        $newRequestStr = HttpConnection::buildRequest($headInfo);
                        $proxyConnection = new HttpConnection();
                        $proxyConnection->setFlag('proxy');
                        
                        $proxyConnection->onConnectFail(function() use($clientConnection, $host, $port) {
                            $clientConnection->send($this->buildProxyFailResponse("Proxy server can't connect to {$host}:$port"))->close();
                        });
    
                        $proxyConnection->beforeConnect(function(Event $e) use($clientConnection, $proxyConnection) {
                            Logger::record("$proxyConnection connecting to {$e->getAddress()}:{$e->getPort()}");
                        });
    
                        $proxyConnection->onConnect(function() use($clientConnection, $proxyConnection, $newRequestStr) {
                            $proxyConnection->send($newRequestStr);
                        });
    
                        $proxyConnection->onClose(function() use($clientConnection, $proxyConnection) {
                            $clientConnection->close();
                        });
                        
                        $proxyConnection->beforeClose(function() use($clientConnection, $proxyConnection) {
                            Logger::record("Connect closed $proxyConnection ForwardSize={$proxyConnection->getForwardSizeHumanReadable()}");
                        });
    
                        $proxyConnection->onRecv(function(Event $e) use($clientConnection) {
                            $clientConnection->send($e->getMessage());
                        });
                        
                        $proxyConnection->connect($ip, $port ? $port : 80);
                }
            });
            
            $clientConnection->onClose(function() use($clientConnection) {
                if($clientConnection->getForwardTo()) {
                    $clientConnection->getForwardTo()->close();
                }
            });
    
            $clientConnection->beforeClose(function() use($clientConnection) {
                Logger::record("connect closed {$clientConnection}");
            });
        });
        
        
        Connection::dispatch();
    }
    
    public function buildProxyFailResponse($msg) {
        return HttpConnection::buildResponse([
            'body' => $msg
        ], HttpConnection::STATUS_REQUEST_TIMEOUT, 'Request timeout');
    }
}
